package com.tn.rtdata.examples.stockticker.server.util;

import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.Set;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.tn.rtdata.DataListener;
import com.tn.rtdata.util.ObservableMap;

/**
 * An implementation of <code>ObservableMap</code> that limits the number of entries.
 */
public class SizeLimitedObservableMap<K, V> implements ObservableMap<K, V>
{
  private static Logger LOGGER = LoggerFactory.getLogger(SizeLimitedObservableMap.class);

  private static final double DEFAULT_EVICTION_SIZE = 0.1;

  private final ObservableMap<K, V> storage;

  private List<K> evictionKeyOrder;
  private int maximumSize;
  private boolean running;

  /**
   * Creates a new <code>SizeLimitedObservableMap</code> initialized with the <code>storage</code> and the <code>maximumSize</code>.
   */
  public SizeLimitedObservableMap(ObservableMap<K, V> storage, int maximumSize)
  {
    this.storage = storage;
    this.maximumSize = maximumSize;
    this.evictionKeyOrder = new ArrayList<K>();
    this.running = true;

    Thread sizeCheckThread = new Thread(new SizeMonitor());
    sizeCheckThread.setDaemon(true);
    sizeCheckThread.setPriority(Thread.MIN_PRIORITY);
    sizeCheckThread.start();
  }

  /**
   * {@inheritDoc}
   */
  @Override
  protected void finalize() throws Throwable
  {
    this.running = false;
    super.finalize();
  }

  /**
   * {@inheritDoc}
   */
  @Override
  public int size()
  {
    return storage.size();
  }

  /**
   * {@inheritDoc}
   */
  @Override
  public boolean isEmpty()
  {
    return storage.isEmpty();
  }

  /**
   * {@inheritDoc}
   */
  @Override
  public boolean containsKey(Object key)
  {
    return storage.containsKey(key);
  }

  /**
   * {@inheritDoc}
   */
  @Override
  public boolean containsValue(Object value)
  {
    return storage.containsValue(value);
  }

  /**
   * {@inheritDoc}
   */
  @Override
  public V get(Object key)
  {
    return storage.get(key);
  }

  /**
   * {@inheritDoc}
   */
  @Override
  public V put(K key, V value)
  {
    evictionKeyOrder.add(key);
    notifyCheckSize();
    return storage.put(key, value);
  }

  /**
   * {@inheritDoc}
   */
  @Override
  public V remove(Object key)
  {
    //noinspection SuspiciousMethodCalls
    evictionKeyOrder.remove(key);
    return storage.remove(key);
  }

  /**
   * {@inheritDoc}
   */
  @Override
  public void putAll(Map<? extends K, ? extends V> map)
  {
    evictionKeyOrder.addAll(map.keySet());
    notify();
    storage.putAll(map);
  }

  /**
   * {@inheritDoc}
   */
  @Override
  public void clear()
  {
    evictionKeyOrder.clear();
    storage.clear();
  }

  /**
   * {@inheritDoc}
   */
  @Override
  public Set<K> keySet()
  {
    return storage.keySet();
  }

  /**
   * {@inheritDoc}
   */
  @Override
  public Collection<V> values()
  {
    return storage.values();
  }

  /**
   * {@inheritDoc}
   */
  @Override
  public Set<Entry<K, V>> entrySet()
  {
    return storage.entrySet();
  }

  /**
   * {@inheritDoc}
   */
  @Override
  public void addDataListener(DataListener<V> dataListener)
  {
    storage.addDataListener(dataListener);
  }

  /**
   * {@inheritDoc}
   */
  @Override
  public void removeDataListener(DataListener<V> dataListener)
  {
    storage.removeDataListener(dataListener);
  }

  /**
   *
   */
  private void notifyCheckSize()
  {
    synchronized(storage)
    {
      storage.notify();
    }
  }

  /**
   * An implementation of <code>Runnable</code> that monitors the size of this <code>SizeLimitedObservableMap</code> removing entries as
   * necessary.
   */
  private class SizeMonitor implements Runnable
  {
    /**
     * {@inheritDoc}
     */
    @Override
    public void run()
    {
      while (running)
      {
        synchronized(storage)
        {
          try
          {
            storage.wait();
            checkSize();
          }
          catch (InterruptedException e)
          {
            //Ignore.
          }
        }
      }
    }

    /**
     * Checks the size, removing entries if the maximum is exceeded.
     */
    private void checkSize()
    {
      if (storage.size() > maximumSize)
      {
        int targetSize = (int)(maximumSize - (maximumSize * DEFAULT_EVICTION_SIZE));

        LOGGER.debug("Evicting {} entries.", storage.size() - targetSize);

        while (evictionKeyOrder.size() > 0 && storage.size() > targetSize)
        {
          storage.remove(evictionKeyOrder.remove(0));
        }
      }
    }
  }
}